package org.internna.expensetracker.mvc;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Currency;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.internna.expensetracker.cache.CacheStore;
import org.internna.expensetracker.model.Account;
import org.internna.expensetracker.model.AccountTransaction;
import org.internna.expensetracker.model.Bill;
import org.internna.expensetracker.model.Subcategory;
import org.internna.expensetracker.model.security.UserDetails;
import org.internna.expensetracker.model.support.IncomeExpense;
import org.internna.expensetracker.model.support.Interval;
import org.internna.expensetracker.model.support.NameValuePair;
import org.internna.expensetracker.util.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/financial/widgets/**")
public final class WidgetController extends AbstractTransactionController {

	@Autowired private CacheStore cache;

    protected void setCache(CacheStore cache) {
		this.cache = cache;
	}

	@RequestMapping("income_vs_expenses/{intervals}")
    public String incomeVsExpenses(@PathVariable String intervals, ModelMap modelMap) {
    	UserDetails user = getCurrentUser();
    	Map<Currency, NameValuePair<BigDecimal, BigDecimal>> data = cache.getIncomeAndExpenses(user, intervals);
    	if (data == null) {
	    	List<AccountTransaction> transactions = getTransactions(user, intervals, null, modelMap);
	    	data = calculateIncomeAndExpenses(transactions);
	    	cache.storeIncomeAndExpenses(user, intervals, data);
    	}
    	modelMap.addAttribute("data", data);
    	modelMap.addAttribute("dataSet", data.entrySet());
    	modelMap.addAttribute("max", getMaxValue(data));
    	return "widgets/incomevsexpenses";
    }

    protected Map<Currency, NameValuePair<BigDecimal, BigDecimal>> calculateIncomeAndExpenses(List<AccountTransaction> transactions) {
    	Map<Currency, NameValuePair<BigDecimal, BigDecimal>> data = new HashMap<Currency, NameValuePair<BigDecimal, BigDecimal>>();
    	if (!CollectionUtils.isEmpty(transactions)) {
            for (AccountTransaction transaction : transactions) {
                BigDecimal amount = transaction.getAmount();
                Currency currency = Currency.getInstance(transaction.getAccount().getLocale());
                if (!data.containsKey(currency)) {
                    data.put(currency, new NameValuePair<BigDecimal, BigDecimal>(BigDecimal.ZERO, BigDecimal.ZERO));
                }
                NameValuePair<BigDecimal, BigDecimal> incomeExpense = data.get(currency);
                if (amount.doubleValue() >= 0) {
                    incomeExpense.setKey(amount.add(incomeExpense.getKey()));
                } else {
                    incomeExpense.setValue(amount.abs().add(incomeExpense.getValue()));
                }
                data.put(currency, incomeExpense);
            }
    	}
    	return data;
    }

    protected Long getMaxValue(Map<Currency, NameValuePair<BigDecimal, BigDecimal>> data) {
    	Long max = 0L;
    	for (Map.Entry<Currency, NameValuePair<BigDecimal, BigDecimal>> dataRow : data.entrySet()) {
            NameValuePair<BigDecimal, BigDecimal> incomeExpense = dataRow.getValue();
            max = Math.max(max, Math.max(incomeExpense.getKey().intValue(), incomeExpense.getValue().intValue()));
    	}
    	return Math.round(max * 1.1);
    }

    @RequestMapping("expenses_by_category/{intervals}")
    public String expensesByCategory(@PathVariable String intervals, ModelMap modelMap) {
    	UserDetails user = getCurrentUser();
    	Interval interval = getInterval(intervals);
    	Map<Currency, Map<Subcategory, BigDecimal>> data = cache.getExpensesByCategory(user, intervals);
    	if (data == null) {
	    	List<AccountTransaction> transactions = getTransactions(interval, null, modelMap);
	    	data = organizeByCategory(transactions);
	    	cache.storeExpensesByCategory(user, intervals, data);
    	}
    	modelMap.addAttribute("data", data);
    	Map<Currency, Set<Map.Entry<Subcategory, BigDecimal>>> dataAsSet = new HashMap<Currency, Set<Map.Entry<Subcategory, BigDecimal>>>();
    	for (Currency currency : data.keySet()) {
            dataAsSet.put(currency, data.get(currency).entrySet());
    	}
    	modelMap.addAttribute("interval", interval);
    	modelMap.addAttribute("dataSet", dataAsSet.entrySet());
    	return "widgets/expenses_by_category";
    }

    protected Map<Currency, Map<Subcategory, BigDecimal>> organizeByCategory(List<AccountTransaction> transactions) {
    	Map<Currency, Map<Subcategory, BigDecimal>> data = new HashMap<Currency, Map<Subcategory, BigDecimal>>(); 
    	if (!CollectionUtils.isEmpty(transactions)) {
            for (AccountTransaction transaction : transactions) {
                Subcategory category = transaction.getSubcategory();
                if (!category.getParentCategory().isIncome()) {
                    BigDecimal amount = transaction.getAmount();
                    Currency currency = Currency.getInstance(transaction.getAccount().getLocale());
                    if (!data.containsKey(currency)) {
                        data.put(currency, new HashMap<Subcategory, BigDecimal>());
                    }
                    if (!data.get(currency).containsKey(category)) {
                        data.get(currency).put(category, BigDecimal.ZERO);
                    }
                    BigDecimal expenses = data.get(currency).get(category);
                    data.get(currency).put(category, expenses.add(amount.abs()));
                }
            }
    	}
    	return data;
    }

    @RequestMapping("expenses_over_time/{months}")
    public String expensesOverTime(@PathVariable int months, ModelMap modelMap) {
    	UserDetails user = getCurrentUser();
        Date[] dates = DateUtils.dates(months);
        Map<Currency, Map<Date, IncomeExpense>> data = cache.getIncomeVsExpensesOverTime(user, months); 
		if (data == null) {
			data = calculateIncomeVsExpensesOverTime(dates, getAccounts());
			cache.storeIncomeVsExpensesOverTime(user, months, data);
		}
        fillModel(dates, data, modelMap);
        return "widgets/expensesovertime";
    }

    @RequestMapping("networth/{months}")
    public String netWorthOverTime(@PathVariable int months, ModelMap modelMap) {
    	UserDetails user = getCurrentUser();
    	Date[] dates = DateUtils.dates(months);
    	Map<Currency, Map<Date, IncomeExpense>> data = cache.getNetWorthOverTime(user, months);
    	if (data == null) {
    		data = calculateWealth(dates, getAccounts());
    		cache.storeNetWorthOverTime(user, months, data);
    	}
    	fillModel(dates, data, modelMap);
    	return "widgets/networth";
    }

    @RequestMapping("income_vs_expenses_over_time/{months}")
    public String incomeVsExpensesOverTime(@PathVariable int months, ModelMap modelMap) {
    	UserDetails user = getCurrentUser();
        Date[] dates = DateUtils.dates(months);
        Map<Currency, Map<Date, IncomeExpense>> data = cache.getIncomeVsExpensesOverTime(user, months);
        if (data == null) {
        	data = calculateIncomeVsExpensesOverTime(dates, getAccounts());
        	cache.storeIncomeVsExpensesOverTime(user, months, data);
        }
        fillModel(dates, data, modelMap);
        return "widgets/incomevsexpensesovertime";
    }

    private void fillModel(Date[] dates, Map<Currency, Map<Date, IncomeExpense>> data, ModelMap modelMap) {
    	Map<Currency, BigDecimal> totalMax = new HashMap<Currency, BigDecimal>();
    	Map<Currency, IncomeExpense> maxValues = new HashMap<Currency, IncomeExpense>();
    	Map<Currency, List<NameValuePair<Date, IncomeExpense>>> dataAsSet = new HashMap<Currency, List<NameValuePair<Date, IncomeExpense>>>();
    	for (Currency currency : data.keySet()) {
    		IncomeExpense max = maxValues.containsKey(currency) ? maxValues.get(currency) : new IncomeExpense();
            List<NameValuePair<Date, IncomeExpense>> orderedData = new ArrayList<NameValuePair<Date, IncomeExpense>>();
            for (Date date : dates) {
            	IncomeExpense value = data.get(currency).get(date);
                max.setKey(max.getKey().doubleValue() > value.getKey().doubleValue() ? max.getKey() : value.getKey());
                max.setValue(max.getValue().doubleValue() > value.getValue().doubleValue() ? max.getValue() : value.getValue());
                orderedData.add(new NameValuePair<Date, IncomeExpense>(date, value));
            }
            maxValues.put(currency, max);
            dataAsSet.put(currency, orderedData);
    	}
    	for (Map.Entry<Currency, IncomeExpense>  entry : maxValues.entrySet()) {
    		totalMax.put(entry.getKey(), entry.getValue().getKey().max(entry.getValue().getValue()));
    	}
    	modelMap.addAttribute("maxValues", maxValues);
    	modelMap.addAttribute("totalMaxValue", totalMax);
    	modelMap.addAttribute("dataSet", dataAsSet.entrySet());
    }

    protected Map<Currency, Map<Date, IncomeExpense>> calculateWealth(Date[] dates, Set<Account> accounts) {
    	Map<Currency, Map<Date, IncomeExpense>> wealthOverTimeAndCurrency = new HashMap<Currency, Map<Date, IncomeExpense>>();
        if (!CollectionUtils.isEmpty(accounts)) {
            for (Account account : accounts) {
                Currency currency = Currency.getInstance(account.getLocale());
                if (!wealthOverTimeAndCurrency.containsKey(currency)) {
                    wealthOverTimeAndCurrency.put(currency, new HashMap<Date, IncomeExpense>());
                }
                Map<Date, IncomeExpense> wealthOverTime = wealthOverTimeAndCurrency.get(currency);
                for (Date month : dates) {
                    if (!wealthOverTime.containsKey(month)) {
                        wealthOverTime.put(month, new IncomeExpense());
                    }
                    IncomeExpense data = wealthOverTime.get(month);
                    data.setKey(data.getKey().add(account.calculateBalance(month)));
                    wealthOverTime.put(month, data);
                }
            }
        }
    	return wealthOverTimeAndCurrency;
    }

    protected Map<Currency, Map<Date, IncomeExpense>> calculateIncomeVsExpensesOverTime(Date[] dates, Set<Account> accounts) {
    	Map<Currency, Map<Date, IncomeExpense>> expensesOverTimeAndCurrency = new HashMap<Currency, Map<Date, IncomeExpense>>();
        if (!CollectionUtils.isEmpty(accounts)) {
            for (Account account : accounts) {
                Currency currency = Currency.getInstance(account.getLocale());
                if (!expensesOverTimeAndCurrency.containsKey(currency)) {
                    expensesOverTimeAndCurrency.put(currency, new HashMap<Date, IncomeExpense>());
                }
                Map<Date, IncomeExpense> incomeExpensesOverTime = expensesOverTimeAndCurrency.get(currency);
                for (Date month : dates) {
                    if (!incomeExpensesOverTime.containsKey(month)) {
                    	incomeExpensesOverTime.put(month, new IncomeExpense());
                    }
                    IncomeExpense monthIncomeExpenses = incomeExpensesOverTime.get(month);
                    List<AccountTransaction> transactions = account.getTransactionsInPeriod(new Interval(DateUtils.getMonthStartDate(month), month), null, true);
                    if (!CollectionUtils.isEmpty(transactions)) {
                        for (AccountTransaction transaction : transactions) {
                            BigDecimal amount = transaction.getAmount();
                            if (amount.doubleValue() >= 0) {
                            	monthIncomeExpenses.setKey(monthIncomeExpenses.getKey().add(amount.abs()));
                            } else {
                            	monthIncomeExpenses.setValue(monthIncomeExpenses.getValue().add(amount.abs()));
                            }
                        }
                    }
                    incomeExpensesOverTime.put(month, monthIncomeExpenses);
                }
            }
        }
        return expensesOverTimeAndCurrency;
    }

    @RequestMapping("remainder/{days}")
    public String remainders(@PathVariable int days, ModelMap modelMap) {
    	UserDetails user = UserDetails.findCurrentUser();
    	modelMap.addAttribute("bills", Bill.findPending(user, days));
    	return "widgets/remainders";
    }

}
